/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.rop.type;

import java.util.HashMap;

/**
 * Representation of a method descriptor. Instances of this class are generally
 * interned and may be usefully compared with each other using {@code ==}.
 */
public final class Prototype implements Comparable<Prototype> {

	/** {@code non-null;} intern table mapping string descriptors to instances */
	private static final HashMap<String, Prototype> internTable = new HashMap<String, Prototype>(
			500);

	/** {@code non-null;} method descriptor */
	private final String descriptor;

	/** {@code non-null;} return type */
	private final Type returnType;

	/** {@code non-null;} list of parameter types */
	private final StdTypeList parameterTypes;

	/** {@code null-ok;} list of parameter frame types, if calculated */
	private StdTypeList parameterFrameTypes;

	/**
	 * Returns the unique instance corresponding to the given method descriptor.
	 * See vmspec-2 sec4.3.3 for details on the field descriptor syntax.
	 * 
	 * @param descriptor
	 *            {@code non-null;} the descriptor
	 * @return {@code non-null;} the corresponding instance
	 * @throws IllegalArgumentException
	 *             thrown if the descriptor has invalid syntax
	 */
	public static Prototype intern(String descriptor) {
		if (descriptor == null) {
			throw new NullPointerException("descriptor == null");
		}

		Prototype result;
		synchronized (internTable) {
			result = internTable.get(descriptor);
		}
		if (result != null) {
			return result;
		}

		Type[] params = makeParameterArray(descriptor);
		int paramCount = 0;
		int at = 1;

		for (;;) {
			int startAt = at;
			char c = descriptor.charAt(at);
			if (c == ')') {
				at++;
				break;
			}

			// Skip array markers.
			while (c == '[') {
				at++;
				c = descriptor.charAt(at);
			}

			if (c == 'L') {
				// It looks like the start of a class name; find the end.
				int endAt = descriptor.indexOf(';', at);
				if (endAt == -1) {
					throw new IllegalArgumentException("bad descriptor");
				}
				at = endAt + 1;
			} else {
				at++;
			}

			params[paramCount] = Type.intern(descriptor.substring(startAt, at));
			paramCount++;
		}

		Type returnType = Type.internReturnType(descriptor.substring(at));
		StdTypeList parameterTypes = new StdTypeList(paramCount);

		for (int i = 0; i < paramCount; i++) {
			parameterTypes.set(i, params[i]);
		}

		result = new Prototype(descriptor, returnType, parameterTypes);
		return putIntern(result);
	}

	/**
	 * Helper for {@link #intern} which returns an empty array to populate with
	 * parsed parameter types, and which also ensures that there is a '(' at the
	 * start of the descriptor and a single ')' somewhere before the end.
	 * 
	 * @param descriptor
	 *            {@code non-null;} the descriptor string
	 * @return {@code non-null;} array large enough to hold all parsed parameter
	 *         types, but which is likely actually larger than needed
	 */
	private static Type[] makeParameterArray(String descriptor) {
		int length = descriptor.length();

		if (descriptor.charAt(0) != '(') {
			throw new IllegalArgumentException("bad descriptor");
		}

		/*
		 * This is a cheesy way to establish an upper bound on the number of
		 * parameters: Just count capital letters.
		 */
		int closeAt = 0;
		int maxParams = 0;
		for (int i = 1; i < length; i++) {
			char c = descriptor.charAt(i);
			if (c == ')') {
				closeAt = i;
				break;
			}
			if ((c >= 'A') && (c <= 'Z')) {
				maxParams++;
			}
		}

		if ((closeAt == 0) || (closeAt == (length - 1))) {
			throw new IllegalArgumentException("bad descriptor");
		}

		if (descriptor.indexOf(')', closeAt + 1) != -1) {
			throw new IllegalArgumentException("bad descriptor");
		}

		return new Type[maxParams];
	}

	/**
	 * Interns an instance, adding to the descriptor as necessary based on the
	 * given definer, name, and flags. For example, an init method has an
	 * uninitialized object of type {@code definer} as its first argument.
	 * 
	 * @param descriptor
	 *            {@code non-null;} the descriptor string
	 * @param definer
	 *            {@code non-null;} class the method is defined on
	 * @param isStatic
	 *            whether this is a static method
	 * @param isInit
	 *            whether this is an init method
	 * @return {@code non-null;} the interned instance
	 */
	public static Prototype intern(String descriptor, Type definer,
			boolean isStatic, boolean isInit) {
		Prototype base = intern(descriptor);

		if (isStatic) {
			return base;
		}

		if (isInit) {
			definer = definer.asUninitialized(Integer.MAX_VALUE);
		}

		return base.withFirstParameter(definer);
	}

	/**
	 * Interns an instance which consists of the given number of {@code int}s
	 * along with the given return type
	 * 
	 * @param returnType
	 *            {@code non-null;} the return type
	 * @param count
	 *            {@code > 0;} the number of elements in the prototype
	 * @return {@code non-null;} the interned instance
	 */
	public static Prototype internInts(Type returnType, int count) {
		// Make the descriptor...

		StringBuffer sb = new StringBuffer(100);

		sb.append('(');

		for (int i = 0; i < count; i++) {
			sb.append('I');
		}

		sb.append(')');
		sb.append(returnType.getDescriptor());

		// ...and intern it.
		return intern(sb.toString());
	}

	/**
	 * Constructs an instance. This is a private constructor; use one of the
	 * public static methods to get instances.
	 * 
	 * @param descriptor
	 *            {@code non-null;} the descriptor string
	 */
	private Prototype(String descriptor, Type returnType,
			StdTypeList parameterTypes) {
		if (descriptor == null) {
			throw new NullPointerException("descriptor == null");
		}

		if (returnType == null) {
			throw new NullPointerException("returnType == null");
		}

		if (parameterTypes == null) {
			throw new NullPointerException("parameterTypes == null");
		}

		this.descriptor = descriptor;
		this.returnType = returnType;
		this.parameterTypes = parameterTypes;
		this.parameterFrameTypes = null;
	}

	/** {@inheritDoc} */
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			/*
			 * Since externally-visible instances are interned, this check helps
			 * weed out some easy cases.
			 */
			return true;
		}

		if (!(other instanceof Prototype)) {
			return false;
		}

		return descriptor.equals(((Prototype) other).descriptor);
	}

	/** {@inheritDoc} */
	@Override
	public int hashCode() {
		return descriptor.hashCode();
	}

	/** {@inheritDoc} */
	public int compareTo(Prototype other) {
		if (this == other) {
			return 0;
		}

		/*
		 * The return type is the major order, and then args in order, and then
		 * the shorter list comes first (similar to string sorting).
		 */

		int result = returnType.compareTo(other.returnType);

		if (result != 0) {
			return result;
		}

		int thisSize = parameterTypes.size();
		int otherSize = other.parameterTypes.size();
		int size = Math.min(thisSize, otherSize);

		for (int i = 0; i < size; i++) {
			Type thisType = parameterTypes.get(i);
			Type otherType = other.parameterTypes.get(i);

			result = thisType.compareTo(otherType);

			if (result != 0) {
				return result;
			}
		}

		if (thisSize < otherSize) {
			return -1;
		} else if (thisSize > otherSize) {
			return 1;
		} else {
			return 0;
		}
	}

	/** {@inheritDoc} */
	@Override
	public String toString() {
		return descriptor;
	}

	/**
	 * Gets the descriptor string.
	 * 
	 * @return {@code non-null;} the descriptor
	 */
	public String getDescriptor() {
		return descriptor;
	}

	/**
	 * Gets the return type.
	 * 
	 * @return {@code non-null;} the return type
	 */
	public Type getReturnType() {
		return returnType;
	}

	/**
	 * Gets the list of parameter types.
	 * 
	 * @return {@code non-null;} the list of parameter types
	 */
	public StdTypeList getParameterTypes() {
		return parameterTypes;
	}

	/**
	 * Gets the list of frame types corresponding to the list of parameter
	 * types. The difference between the two lists (if any) is that all
	 * "intlike" types (see {@link Type#isIntlike}) are replaced by
	 * {@link Type#INT}.
	 * 
	 * @return {@code non-null;} the list of parameter frame types
	 */
	public StdTypeList getParameterFrameTypes() {
		if (parameterFrameTypes == null) {
			int sz = parameterTypes.size();
			StdTypeList list = new StdTypeList(sz);
			boolean any = false;
			for (int i = 0; i < sz; i++) {
				Type one = parameterTypes.get(i);
				if (one.isIntlike()) {
					any = true;
					one = Type.INT;
				}
				list.set(i, one);
			}
			parameterFrameTypes = any ? list : parameterTypes;
		}

		return parameterFrameTypes;
	}

	/**
	 * Returns a new interned instance, which is the same as this instance,
	 * except that it has an additional parameter prepended to the original's
	 * argument list.
	 * 
	 * @param param
	 *            {@code non-null;} the new first parameter
	 * @return {@code non-null;} an appropriately-constructed instance
	 */
	public Prototype withFirstParameter(Type param) {
		String newDesc = "(" + param.getDescriptor() + descriptor.substring(1);
		StdTypeList newParams = parameterTypes.withFirst(param);

		newParams.setImmutable();

		Prototype result = new Prototype(newDesc, returnType, newParams);

		return putIntern(result);
	}

	/**
	 * Puts the given instance in the intern table if it's not already there. If
	 * a conflicting value is already in the table, then leave it. Return the
	 * interned value.
	 * 
	 * @param desc
	 *            {@code non-null;} instance to make interned
	 * @return {@code non-null;} the actual interned object
	 */
	private static Prototype putIntern(Prototype desc) {
		synchronized (internTable) {
			String descriptor = desc.getDescriptor();
			Prototype already = internTable.get(descriptor);
			if (already != null) {
				return already;
			}
			internTable.put(descriptor, desc);
			return desc;
		}
	}
}
